package de.mmis.core.base;

import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.log4j.Logger;

import de.mmis.core.base.DeserializableAnnotations.DeserializationMethod;
import de.mmis.core.base.abstracttree.InnerNode;
import de.mmis.core.base.abstracttree.LeafNode;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.Serializable;

/**
 * auxiliary class to convert abstract type names to java classes and vice
 * versa. An object of this class represents an abstract type, which may has a
 * java class equivalent
 * 
 * @author Christian Koch
 * 
 */
public class Type implements Serializable {

	public static @interface TypeName {
		String value();
	}

	private final static Logger LOGGER = Logger.getLogger(Type.class);

	private String abstractName;
	private String javaClassName;
	private Class<?> javaClass;

	/**
	 * create new Type using Type.class' ClassLoader to find abstract name
	 * 
	 * @param clazz
	 *            underlying java class
	 */
	public Type(Class<?> clazz) {
		this(clazz, Type.class.getClassLoader());
	}

	/**
	 * create new Type using given ClassLoader to find abstract name
	 * 
	 * @param clazz
	 *            underlying java class
	 * @param classLoader
	 *            class loader to use
	 */
	public Type(Class<?> clazz, ClassLoader classLoader) {
		this.javaClass = clazz;
		this.javaClassName = javaClass.getName();
		this.abstractName = getAbstractNameFromClassName(javaClassName,
				classLoader);
		if (this.abstractName == null)
			this.abstractName = this.javaClassName;
	}

	/**
	 * create type from given name using Type.class' ClassLoader to find java
	 * class. It can be either an abstract type name, or a java class name.
	 * 
	 * @param name
	 *            name of the type
	 */
	public Type(String name) {
		this(name, Type.class.getClassLoader());
	}

	/**
	 * create type from given name. It can be either an abstract type name, or a
	 * java class name.
	 * 
	 * @param name
	 *            name of the type
	 * @param classLoader
	 *            class loader to use when loading classes
	 */
	public Type(String name, ClassLoader classLoader) {
		this.abstractName = name;
		this.javaClassName = getClassNameFromAbstractName(name, classLoader);
		if (this.javaClassName == null)
			this.javaClassName = this.abstractName;
		try {
			this.javaClass = Toolbox.forName(this.javaClassName, classLoader);
		} catch (ClassNotFoundException e) {
			LOGGER.debug("class " + name + " cannot be found", e);
			this.javaClass = null;
		}
	}

	/**
	 * convert the abstract type name to a java class name (if possible).
	 * 
	 * @param typeIdentifier
	 *            abstract name to convert
	 * @param classLoader
	 *            class loader to use
	 * @return the java class name corresponding to the abstract name, or
	 *         <code>null</code>, if it is not possible.
	 */
	public static String getClassNameFromAbstractName(String typeIdentifier,
			ClassLoader classLoader) {
		InputStream instream = classLoader
				.getResourceAsStream("de/mmis/core/base/types/abstracttype/"
						+ typeIdentifier);
		if (instream == null) {
			LOGGER.trace("type properties file not found for type "
					+ typeIdentifier);
			return null;
		}

		// read properties file
		Properties p = new Properties();
		try {
			p.load(instream);
		} catch (IOException e) {
			LOGGER.warn("type properties file cannot be read for type "
					+ typeIdentifier);
			return null;
		}

		// get (any) class name in this property map
		Set<String> keys = p.stringPropertyNames();
		if (keys.isEmpty()) {
			LOGGER.warn("type properties file for type " + typeIdentifier
					+ " is empty");
			return null;
		}
		return keys.iterator().next();
	}

	/**
	 * convert the java class name to an abstract type name (if possible).
	 * 
	 * @param className
	 *            abstract name to convert
	 * @param classLoader
	 *            class loader to use
	 * @return the abstract type name corresponding to the java class name, or
	 *         <code>null</code>, if it is not possible.
	 */
	public static String getAbstractNameFromClassName(String className,
			ClassLoader classLoader) {
		InputStream instream = classLoader
				.getResourceAsStream("de/mmis/core/base/types/javaclass/"
						+ className);
		if (instream == null) {
			LOGGER.trace("type properties file not found for java class "
					+ className);
			return null;
		}

		// read properties file
		Properties p = new Properties();
		try {
			p.load(instream);
		} catch (IOException e) {
			LOGGER.warn("type properties file cannot be read for java class "
					+ className);
			return null;
		}

		// get (any) class name in this property map
		Set<String> keys = p.stringPropertyNames();
		if (keys.isEmpty()) {
			LOGGER.warn("type properties file for java class " + className
					+ " is empty");
			return null;
		}
		return keys.iterator().next();
	}

	/**
	 * convert a java class to an abstract name (if present)
	 * 
	 * @param clazz
	 *            java class to convert
	 * @param classLoader
	 *            class loader to use
	 * @return the resulting abstract name, or the java class name, if it is not
	 *         possible.
	 */
	public static String getAbstractNameFromClass(Class<?> clazz,
			ClassLoader classLoader) {
		String ret = getAbstractNameFromClassName(clazz.getName(), classLoader);
		if (ret == null)
			return clazz.getName();
		return ret;
	}

	/**
	 * @return the java class of the type, or <code>null</code>, if it cannot be
	 *         found
	 */
	public Class<?> getJavaClass() {
		return javaClass;
	}

	/**
	 * @return the java class name of this type, which is equal to the abstract
	 *         name if no corresponding java class name is found
	 */
	public String getJavaClassName() {
		return javaClassName;
	}

	/**
	 * @return the abstract name of this type
	 */
	public String getAbstractName() {
		return abstractName;
	}

	@Override
	public String getTypeIdentifier() {
		return "type";
	}

	@Override
	public Tree[] serialize(boolean explicit, Map<Object, String> refmap,
			ClassLoader classLoader) {
		return new Tree[] { new LeafNode(abstractName) };
	}

	@DeserializationMethod
	public static Type deserialize(Tree[] e, ClassLoader classLoader,
			String ownID, Map<String, Object> idmap)
			throws DeserializableException {
		if (e.length != 1 || e[0].getType() != Tree.Type.LEAF)
			throw new DeserializableException(new InnerNode(e),
					"Interface allows only 1 LeafNode argument");

		return new Type(((LeafNode) e[0]).getUTF8Data(), classLoader);
	}

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result
				+ ((abstractName == null) ? 0 : abstractName.hashCode());
		result = prime * result
				+ ((javaClass == null) ? 0 : javaClass.hashCode());
		result = prime * result
				+ ((javaClassName == null) ? 0 : javaClassName.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Type other = (Type) obj;
		if (abstractName == null) {
			if (other.abstractName != null)
				return false;
		} else if (!abstractName.equals(other.abstractName))
			return false;
		if (javaClass == null) {
			if (other.javaClass != null)
				return false;
		} else if (!javaClass.equals(other.javaClass))
			return false;
		if (javaClassName == null) {
			if (other.javaClassName != null)
				return false;
		} else if (!javaClassName.equals(other.javaClassName))
			return false;
		return true;
	}
}
